package uk.ac.aber.paws.server.ldap;

import java.util.*;
import java.util.logging.Level;

import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.ldap.*;

import java.net.*;
import uk.ac.aber.paws.server.config.*;
import uk.ac.aber.paws.server.core.PAWSServer;
import uk.ac.aber.paws.server.db.DBLogger;
import uk.ac.aber.paws.server.util.TextManip;

/**
 * @author Rob Johnson, Aberystwyth University
 * @email rob.johnson@aber.ac.uk
 * @date 21st May 2012
 * 
 * LDAPQuery.java
 * 
 * A class that contains all the LDAP related operations.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
public class LDAPQuery {

	public static final int OU_LEVEL_IDENTIFIER = 1;
	public static final int COMPUTER_LEVEL_IDENTIFIER = 2;
		
	/**
	 * Checks whether the given OU is a computer object or organizationalUnit object. If sub OUs are discovered then it is classed as an OU
	 * @param ldapPath The full OU path
	 * @return LDAPQuery.COMPUTER_LEVEL_IDENTIFIER is returned if a computer object,
	 *  LDAPQuery.OU-LEVEL_IDENTIFIER is returned if a organizationUnit object 
	 */
	public static int ouCheck(String ldapPath, String server, String protocol, String port, String username, String password, String dc, boolean includeDomainInUserCreds, String method) {
	
		String searchBase = ldapPath;
		
        String returnedAtts[] ={"distinguishedName","cn" };
        String searchFilter = "(objectClass=*)";

        SearchControls searchCtls = new SearchControls();
		searchCtls.setReturningAttributes(returnedAtts);

		searchCtls.setSearchScope(SearchControls.ONELEVEL_SCOPE);

		Hashtable<String,String> environment = new Hashtable<String,String>();
		environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
	
		environment.put(Context.PROVIDER_URL, protocol+"://" + server + ":"+port);
	
		environment.put(Context.SECURITY_AUTHENTICATION, method);
		
		if (includeDomainInUserCreds) {
			environment.put(Context.SECURITY_PRINCIPAL, username + "@" + dc);
		}
		else {
			environment.put(Context.SECURITY_PRINCIPAL, username);
		}
		
		environment.put(Context.SECURITY_CREDENTIALS, password);

		LdapContext ctxGC = null;
              
        int subtreeCount = 0;
        
        try {
	        	
			ctxGC = new InitialLdapContext(environment, null);

			NamingEnumeration<SearchResult> answer = ctxGC.search(searchBase, searchFilter, searchCtls);

			while (answer.hasMoreElements()) {
				answer.nextElement();
				subtreeCount++;
			}
			
			int baseCount = 0;
			searchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
			
			ctxGC = new InitialLdapContext(environment, null);

			answer = ctxGC.search(searchBase, searchFilter, searchCtls);

			while (answer.hasMoreElements()) {
				answer.nextElement();
				baseCount++;
			}
			
			if (subtreeCount > 0 && baseCount == 1) {
				return OU_LEVEL_IDENTIFIER;
			}
			else if (subtreeCount == 0 && baseCount == 1) {
				return COMPUTER_LEVEL_IDENTIFIER;
			}
			
		}
		catch (CommunicationException e) {
			System.out.println("FAILED LDAP");
		}
		catch (NamingException e) {
            e.printStackTrace();
		}
		
		return -1;
	}
	
	/**
	 * Converts the ldap path into a dot separated fully qualified domain name
	 * e.g. cn=test,ou=topou,dc=staff,dc=aber,dc=ac,dc=uk becomes 
	 * test.topou.staff.aber.ac.uk (if includeObjectName is true) or
	 * topou.staff.aber.ac.uk (if includeObjectName is false)
	 * @param ou The full ldap path to the object
	 * @param includeObjectName If true, includes the bottom level object in the fqdn, if false, 1st node is the object's parent ou
	 * @return The fqdn representation of this ldap path
	 */
	public static String convertToFQDN(String ou, boolean includeObjectName) {
		
		String fqdn = "";
		ou = ou.toLowerCase();
		
		StringTokenizer st = new StringTokenizer(ou,",");
		
		while (st.hasMoreElements()) {
			
			StringTokenizer st2 = new StringTokenizer((String)st.nextElement(),"=");
				
			if (st2.countTokens() == 2) {
				
				String identifier = ((String)st2.nextElement()).toUpperCase();
				
				if (identifier.equals("DC")) {
					fqdn += (String)st2.nextElement()+".";
				}
				
				if (includeObjectName && identifier.equals("CN")) {
					fqdn = (String)st2.nextElement()+".";
				}
			}
		}	
		
		return fqdn.substring(0,fqdn.length()-1).toUpperCase();
	}

	/**
	 * Returns a list of objects in the specified part of the ldap tree 	
	 * @param domain The domain of the ldap tree to query e.g. staff.aber.ac.uk
	 * @param searchBase The base ldap path in which to commence the search downwards in the tree
	 * @param objectType The object type filter e.g. computer or organizationalUnit
	 * @return A vector of the full ldap paths of all objects found
	 */
	public static Vector<String> ouLookup(String server, String protocol, String port, String username, String password, String dc, boolean includeDomainInUserCreds, String method, String searchBase, String objectType, int scope) {
	
        String returnedAtts[] ={"distinguishedName"};
        String searchFilter = "(&(objectclass="+objectType+"))";
        
        SearchControls searchCtls = new SearchControls();
		searchCtls.setReturningAttributes(returnedAtts);

		searchCtls.setSearchScope(scope);
		
		Hashtable<String,String> environment = new Hashtable<String,String>();
		environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
	
		environment.put(Context.PROVIDER_URL, protocol+"://" + server + ":"+port);
	
		environment.put(Context.SECURITY_AUTHENTICATION, method);
		environment.put(Context.SECURITY_PRINCIPAL, username);
		
		
		if (includeDomainInUserCreds) {
			environment.put(Context.SECURITY_PRINCIPAL, username + "@" + dc);
		}
		else {
			environment.put(Context.SECURITY_PRINCIPAL, username);
		}
		
		environment.put(Context.SECURITY_CREDENTIALS, password);

		LdapContext ctxGC = null;

        Vector<String> dataVector = new Vector<String>();
        
        int count = 0;
        boolean successful = false;
        
        while (!successful && count < 60) {
        	
	        try {
	        	
				ctxGC = new InitialLdapContext(environment, null);
	
				NamingEnumeration<SearchResult> answer = ctxGC.search(searchBase, searchFilter, searchCtls);
	
				while (answer.hasMoreElements()) {
	        	   	SearchResult sr = (SearchResult)answer.next();
					Attributes attrs = sr.getAttributes();
					
					String distinguishedName = attrs.get("distinguishedName").toString().replace("distinguishedName:","");
			
					distinguishedName = distinguishedName.trim();
					
					dataVector.add(distinguishedName);	
				}
					
				successful = true;
			}
			catch (CommunicationException e) {
				PAWSServer.LOG_FILE.log(Level.INFO,"LDAP Problem = "+searchBase+".."+e.toString());
		        
			}
			catch (NamingException e) {
				PAWSServer.LOG_FILE.log(Level.INFO,"LDAP Problem = "+searchBase+".."+e.toString());
	            e.printStackTrace();
			}
			catch (Exception e) {
				PAWSServer.LOG_FILE.log(Level.INFO,"LDAP Problem = "+searchBase+".."+e.toString());
				e.printStackTrace();
			}
			
			try {
				Thread.currentThread().sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			count++;
        }
        
        if (count == 60) {
        	PAWSServer.LOG_FILE.log(Level.INFO,"LDAP Querying failed for = "+searchBase);
        }
        
		return dataVector;	        

	}
	
	/**
	 * Returns a list of bottom level OUs under all the specified top level ldap paths. This may
	 * be required in order to harvest bottom level OUs for statistics generation
	 * @return A Hashtable, indexed by the top level OU with a subsequent Vector containing a list of all the lowest level OUs present
	 */
	public static Hashtable<String, Vector<String>> getAllBottomOUs(Vector<Hashtable<String,Object>> ouList) {

		Hashtable<String,Vector<String>> bottomOUs = new Hashtable<String,Vector<String>>();
		
		// for each root OU
		for (int i=0;i<ouList.size();i++) {
		
			String topOU = (String)ouList.get(i).get("StatsRootOU");
			
			String domainName = LDAPQuery.convertToFQDN(topOU,false).toLowerCase();
			
			Vector<String> objects = LDAPQuery.ouLookup(ConfigManager.getProperty("app.ldap.loginserver."+domainName),ConfigManager.getProperty("app.ldap.protocol."+domainName),ConfigManager.getProperty("app.ldap.port."+domainName),ConfigManager.getProperty("app.ldap.username."+domainName),ConfigManager.getProperty("app.ldap.password."+domainName),ConfigManager.getProperty("app.ldap.logindomain."+domainName),true,ConfigManager.getProperty("app.ldap.method."+domainName), topOU, "computer", SearchControls.SUBTREE_SCOPE);

			bottomOUs.putAll(parseLdapObjects(objects));
			
		}
		
		return bottomOUs;
	}

	/**
	 * Extracts the names of the bottom OUs from the full context paths that have been provided.
	 * @param objects A vector of bottom level context paths
	 * @return A list of extracted ou names
	 */
	private static Hashtable<String,Vector<String>> parseLdapObjects(Vector<String> objects) {
		
		Hashtable<String,Vector<String>> returnData = new Hashtable<String,Vector<String>>();
		
		for (int i=0;i<objects.size();i++) {
			
			String thisObj = objects.get(i).toUpperCase();
			
			String extractedOU = thisObj.substring(thisObj.indexOf("OU="), thisObj.length());
		
			if (returnData.get(extractedOU) == null) {
				returnData.put(extractedOU, new Vector<String>());
			}
			
			String fqdn = TextManip.convertToLDAPtoFQDN(thisObj);	
			
			returnData.get(extractedOU).add(fqdn);
		}
		
		return returnData;
	}
	
}
